package jmine.tec.services.export.impl;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import jmine.tec.persist.schema.impl.elements.SequenceElement;
import jmine.tec.services.annotations.ServiceImplementor;
import jmine.tec.services.api.ActionsEnum;
import jmine.tec.services.api.Service;
import jmine.tec.services.api.export.ExportAction;
import jmine.tec.services.api.export.ExportTarget;
import jmine.tec.services.api.export.ReferenceMap;
import jmine.tec.services.io.ServiceBean;

import org.hibernate.proxy.HibernateProxyHelper;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.base.enumx.IntValuedEnum;
import bancosys.tec.persist.base.enumx.StringValuedEnum;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.services.api.AbstractService;
import bancosys.tec.utils.date.Date;
import bancosys.tec.utils.date.DateUtils;
import bancosys.tec.utils.date.Timestamp;

/**
 * Métodos utilitários para implementações de EntityExporter
 * 
 * @author lundberg
 */
public final class ExportUtils {

    /**
     * Construtor.
     */
    private ExportUtils() {

    }

    /**
     * Retorna o nome do serviço, dada a classe que o implementa. Essa classe pode tanto ser uma implementação da interface Service (infra
     * antiga ser serviço) quanto uma classe anotada com ServiceImplementor
     * 
     * @param serviceClass serviceClass
     * @return service name
     */
    public static String getServiceName(Class<?> serviceClass) {
        if (serviceClass.isAnnotationPresent(ServiceImplementor.class)) {
            ServiceImplementor implementorAnnotation = serviceClass.getAnnotation(ServiceImplementor.class);
            if (implementorAnnotation.serviceName().length() != 0) {
                return implementorAnnotation.serviceName();
            } else {
                return AbstractService.getServiceName(serviceClass);
            }
        } else if (Service.class.isAssignableFrom(serviceClass)) {
            try {
                Service<?> service = (Service<?>) serviceClass.newInstance();
                return service.getServiceName();
            } catch (InstantiationException e) {
                throw new IllegalArgumentException("Failed to instantiate service to retrieve name.", e);
            } catch (IllegalAccessException e) {
                throw new IllegalArgumentException("Failed to instantiate service to retrieve name.", e);
            }
        } else {
            throw new IllegalArgumentException("Given class is not a service.");
        }
    }

    /**
     * Retorna a ação do serviço, dada a classe que o implementa. Essa classe pode tanto ser uma implementação da interface Service (infra
     * antiga ser serviço) quanto uma classe anotada com ServiceImplementor
     * 
     * @param serviceClass serviceClass
     * @return service action
     */
    public static ActionsEnum getServiceAction(Class<?> serviceClass) {
        if (serviceClass.isAnnotationPresent(ServiceImplementor.class)) {
            ServiceImplementor implementorAnnotation = serviceClass.getAnnotation(ServiceImplementor.class);
            return implementorAnnotation.action();
        } else if (Service.class.isAssignableFrom(serviceClass)) {
            try {
                Service<?> service = (Service<?>) serviceClass.newInstance();
                return service.getAction();
            } catch (InstantiationException e) {
                throw new IllegalArgumentException("Failed to instantiate service to retrieve name.", e);
            } catch (IllegalAccessException e) {
                throw new IllegalArgumentException("Failed to instantiate service to retrieve name.", e);
            }
        } else {
            throw new IllegalArgumentException("Given class is not a service.");
        }
    }

    /**
     * Adiciona o valor ao ServiceBean com a chave dada.
     * 
     * @param bean bean
     * @param key key
     * @param value value
     */
    public static void put(ServiceBean bean, String key, String value) {
        if (value == null) {
            return;
        }
        bean.putParameter(key, value);
    }

    /**
     * Adiciona o valor ao ServiceBean com a chave dada.
     * 
     * @param bean bean
     * @param key key
     * @param value value
     */
    public static void put(ServiceBean bean, String key, Date value) {
        if (value == null) {
            return;
        }
        bean.putParameter(key, DateUtils.dateToStringDDMMYYYY(value));
    }

    /**
     * Adiciona o valor ao ServiceBean com a chave dada.
     * 
     * @param bean bean
     * @param key key
     * @param value value
     */
    public static void put(ServiceBean bean, String key, Timestamp value) {
        if (value == null) {
            return;
        }
        bean.putParameter(key, DateUtils.timestampToString(value));
    }

    /**
     * Adiciona o valor ao ServiceBean com a chave dada.
     * 
     * @param bean bean
     * @param key key
     * @param value value
     */
    public static void put(ServiceBean bean, String key, BigDecimal value) {
        if (value == null) {
            return;
        }
        bean.putParameter(key, value.toPlainString());
    }

    /**
     * Adiciona o valor ao ServiceBean com a chave dada.
     * 
     * @param bean bean
     * @param key key
     * @param value value
     */
    public static void put(ServiceBean bean, String key, Enum<?> value) {
        if (value == null) {
            return;
        }
        bean.putParameter(key, value.toString());
    }

    /**
     * Adiciona o valor ao ServiceBean com a chave dada.
     * 
     * @param bean bean
     * @param key key
     * @param value value
     */
    public static void put(ServiceBean bean, String key, StringValuedEnum value) {
        if (value == null) {
            return;
        }
        bean.putParameter(key, value.getName());
    }

    /**
     * Adiciona o valor ao ServiceBean com a chave dada.
     * 
     * @param bean bean
     * @param key key
     * @param value value
     */
    public static void put(ServiceBean bean, String key, IntValuedEnum value) {
        if (value == null) {
            return;
        }
        bean.putParameter(key, value.getName());
    }

    /**
     * Adiciona o valor ao ServiceBean com a chave dada.
     * 
     * @param bean bean
     * @param key key
     * @param value value
     */
    public static void put(ServiceBean bean, String key, Object value) {
        if (value == null) {
            return;
        }
        bean.putParameter(key, value.toString());
    }

    /**
     * Define a ação e o nome do serviço, partindo da classe do serviço
     * 
     * @param bean bean
     * @param serviceClass serviceClass
     */
    public static void setService(ServiceBean bean, Class<?> serviceClass) {
        bean.setAction(getServiceAction(serviceClass));
        bean.setName(getServiceName(serviceClass));
    }

    /**
     * Adiciona ao service bean o id da entidade, caso este não tenha sido gerado
     * 
     * @param bean bean
     * @param key key
     * @param entity entity
     */
    public static void putDatabaseId(ServiceBean bean, String key, Persistable entity) {
        if (entity.getPk() instanceof Long) {
            Long id = (Long) entity.getPk();
            if (id < SequenceElement.DEFAULT_INITIAL_VALUE) {
                put(bean, key, id);
            }
        }
    }

    /**
     * Adiciona o parâmetro da entidade. Caso exista referencia, adiciona a referência, caso contrário utiliza a chave natural.
     * 
     * @param bean bean
     * @param key key
     * @param referenceMap referenceMap
     * @param entity entity
     * @param naturalKey naturalKey
     */
    public static void put(ServiceBean bean, String key, ReferenceMap referenceMap, Persistable entity, String naturalKey) {
        if (referenceMap.containsReference(entity)) {
            put(bean, key, referenceMap.getReference(entity));
        } else {
            put(bean, key, naturalKey);
        }
    }

    /**
     * Prepara para exportação a lista de entidades.
     * 
     * @param action action
     * @param entities entities
     * @return List de ExportTarget
     */
    public static List<ExportTarget> wrapList(ExportAction action, List<?> entities) {
        List<ExportTarget> list = new ArrayList<ExportTarget>();
        for (Object entity : entities) {
            list.add(wrap(action, entity));
        }
        return list;
    }

    /**
     * Prepara a entidade para exportação com a dada action.
     * 
     * @param action action
     * @param entity entity
     * @return ExportTarget
     */
    public static ExportTarget wrap(ExportAction action, Object entity) {
        return new DefaultExportTarget(action, entity);
    }

    /**
     * Efetua o narrow da entidade
     * 
     * @param <E> tipo da entidade
     * @param entityClass entityClass
     * @param entity entity
     * @return entity
     */
    public static <E extends Persistable> E narrow(Class<E> entityClass, E entity) {
        BaseDAO<E> dao = entity.getController().getDAOFactory().getGenericDAO(entityClass);
        return dao.narrow(entity);
    }

    /**
     * Efetua o narrow da entidade, caso seja persistível e seja possível. Caso contrário não faz nada.
     * 
     * @param obj obj
     * @return Object
     */
    @SuppressWarnings("unchecked")
    public static Object narrow(Object obj) {
        if (obj == null) {
            return null;
        }
        if (!Persistable.class.isAssignableFrom(obj.getClass())) {
            return obj;
        } else {
            Persistable persistable = (Persistable) obj;
            if (persistable.getPk() == null) {
                return persistable;
            }
            return narrow(HibernateProxyHelper.getClassWithoutInitializingProxy(obj), (Persistable) obj);
        }
    }
}
